﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.ComponentModel;
using System.Xml.Serialization;
using System.Security.Cryptography;

namespace ChromaticQFX
{

    public class QFXHistory
    {
        enum ParsingStateEnum { INIT, ORG, TRANS, LEDGERBAL };

        private string _Organization = "";
        [Description("A code representing an Organization")]
        public string Organization { get { return _Organization; } set { _Organization = value; } }

        private string _Currency = "";
        [Description("A code designating the currency which financial amounts represent")]
        public string Currency { get { return _Currency; } set { _Currency = value; } }

        private string _BankID = "";
        [Description("A unique id representing a financial institution")]
        public string BankID { get { return _BankID; } set { _BankID = value; } }

        private string _AccountID = "";
        [Description("A unique id representing a financial account")]
        public string AccountID { get { return _AccountID; } set { _AccountID = value; } }

        private string _AccountType = "";
        [Description("An account type imported from a qfx data file")]
        public string AccountType { get { return _AccountType; } set { _AccountType = value; } }

        private string _IntuitBankID = "";
        [Description("A unique ID representing the financial institution for qfx data")]
        public string IntuitBankID { get { return _IntuitBankID; } set { _IntuitBankID = value; } }

        private Decimal _Balance = 0.0m;
        [Description("The final balance of the first import")]
        public Decimal Balance { get { return _Balance; } set { _Balance = value; } }

        private DateTime _BalanceDate = DateTime.MinValue;
        [Description("The final balance date of the first import")]
        public DateTime BalanceDate { get { return _BalanceDate; } set { _BalanceDate = value; } }

        private List<QFXTransaction> _Transactions = new List<QFXTransaction>();
        [Description("A collection of transactions, derived from qfx imports")]
        public List<QFXTransaction> Transactions { get { return _Transactions; } set { _Transactions = value; } }

        private List<string> _Categories = new List<string>();
        [Description("A collection of categories with which transactions can be designated")]
        public List<string> Categories { get { return _Categories; } set { _Categories = value; } }

        private DateTime _StartDate = DateTime.MinValue;
        [Description("The first transaction date of the most recent import")]
        public DateTime StartDate { get { return _StartDate; } set { _StartDate = value; } }

        private DateTime _EndDate = DateTime.MinValue;
        [Description("The last transaction date of the most recent import")]
        public DateTime EndDate { get { return _EndDate; } set { _EndDate = value; } }

        public QFXHistory()
        {
        }

        #region Load/Save History

        public static QFXHistory LoadHistory(string Filename)
        {
            QFXHistory newSettings;

            XmlSerializer s = new XmlSerializer(typeof(QFXHistory));
            TextReader r = new StreamReader(Filename);
            newSettings = (QFXHistory)s.Deserialize(r);
            r.Close();

            return newSettings;
        }

        [Obsolete]
        public string SaveHistory(string Filename)
        {
            XmlSerializer s = new XmlSerializer(this.GetType());
            TextWriter w = new StreamWriter(Filename);
            s.Serialize(w, this);
            w.Close();

            return Filename;
        }

        // NOTE: Encyption here is being done purely for Obfuscation... since i hardcode the password anyone with chromatic can load it
        // but this will keep people from being able to view the file (from perhaps malware or from exploring your hard drive).

        // Encryption is derived from samples here :
        // http://www.christec.co.nz/blog/archives/81
        // http://www.discussweb.com/other-web-programming-languages/2091-uses-symmetric-key-algorithm-rijndael-encrypt-decrypt-data.html
        public static QFXHistory LoadHistoryX()
        {
            string Password = "cR0m@t!K31095";

            QFXHistory newSettings;
            XmlSerializer s = new XmlSerializer(typeof(QFXHistory));

            byte[] rgbIV = System.Text.ASCIIEncoding.UTF8.GetBytes("1150845670772664");  // some random number
            byte[] saltValueBytes = Encoding.ASCII.GetBytes("s0d1uMv4l");

            // Configure the desired encryption algorithm parameters
            SymmetricAlgorithm sa = new RijndaelManaged();

            PasswordDeriveBytes pdb = new PasswordDeriveBytes(Password, saltValueBytes, "SHA1", 2);

            // Use the password to generate pseudo-random bytes for the encryption
            // key. Specify the size of the key in bytes (instead of bits).
            byte[] keyBytes = pdb.GetBytes(256 / 8);

            sa.Mode = CipherMode.CBC;

            // Decryption example
            ICryptoTransform transform = sa.CreateDecryptor(keyBytes, rgbIV);
            using (Stream inputStream = new FileStream(MainForm.ConfigFile, FileMode.Open))
            {
                // Wrap the input stream up with a CryptoStream
                // which performs the data decryption
                using (Stream cryptoStream = new CryptoStream(inputStream, transform, CryptoStreamMode.Read))
                {
                    // Read data into the cryptoStream (which will fetch encrypted
                    // data from the inputStream and then decrypt it before returning
                    // it to us)
                    using (TextReader tr = new StreamReader(cryptoStream))
                        newSettings = (QFXHistory)s.Deserialize(tr); ;
                }
            }

            return newSettings;
        }

        // Encryption is derived from samples here :
        // http://www.christec.co.nz/blog/archives/81
        // http://www.discussweb.com/other-web-programming-languages/2091-uses-symmetric-key-algorithm-rijndael-encrypt-decrypt-data.html
        public void SaveHistoryX()
        {
            string Password = "cR0m@t!K31095";

            XmlSerializer s = new XmlSerializer(this.GetType());
            //byte[] rgbKey = System.Text.ASCIIEncoding.UTF8.GetBytes(Password);
            byte[] rgbIV = System.Text.ASCIIEncoding.UTF8.GetBytes("1150845670772664");  // some random number
            byte[] saltValueBytes = Encoding.ASCII.GetBytes("s0d1uMv4l");

            // Configure the desired encryption algorithm parameters
            SymmetricAlgorithm sa = new RijndaelManaged();

            PasswordDeriveBytes pdb = new PasswordDeriveBytes(Password, saltValueBytes, "SHA1", 2);

            // Use the password to generate pseudo-random bytes for the encryption
            // key. Specify the size of the key in bytes (instead of bits).
            byte[] keyBytes = pdb.GetBytes(256 / 8);


            sa.Mode = CipherMode.CBC;

            // Encryption Example
            ICryptoTransform transform = sa.CreateEncryptor(keyBytes, rgbIV);
            using (Stream outputStream = new FileStream(MainForm.ConfigFile, FileMode.Create))
            {
                // Wrap the output stream up with a CryptoStream
                // which performs the data encryption
                using (Stream cryptoStream = new CryptoStream(outputStream, transform, CryptoStreamMode.Write))
                {
                    // Store data into the cryptoStream (which will encrypt it
                    // and then pass it along to our outputStream for storage)
                    using (TextWriter tw = new StreamWriter(cryptoStream))
                    {
                        s.Serialize(tw, this);
                    }
                }
            }
        }


        #endregion

        #region Merging

        public void Merge(QFXHistory history)
        {
            List<QFXTransaction> filtered = new List<QFXTransaction>();

            foreach(QFXTransaction trans in history.Transactions) 
            {
                if (trans.Name == "ChromaticReconcile") filtered.Add(trans);
                else
                {
                    if (FindTransaction(trans.FitID) == null) filtered.Add(trans);
                }
            }

            Transactions.AddRange(filtered);

            Transactions.Sort(new TransactionComparer());

            // Make the newly merged data's timeframe the default window for graphing
            StartDate = history.StartDate;
            EndDate = history.EndDate;
        }

        QFXTransaction FindTransaction(string FitID)
        {
            return Transactions.Find(delegate(QFXTransaction t) { return t.FitID == FitID; });
        }

        #endregion

        #region Parsing Routines

        public void ParseFile(string Filename)
        {
            string QFXString = "";

            TextReader tr = new StreamReader(Filename);
            QFXString = tr.ReadToEnd();
            tr.Close();

            ParseString(QFXString);

            // Each import gives only one absolute balance, and that is at the very end
            // I need to calculate beginning balance and then encode both 'reconcile'
            // balances into the Transactions list.

            // with accurate beginning balance reconcile i can merge multiple imports
            // into a single list and still be able to accurately determine a running
            // balance.

            Transactions.Sort(new TransactionComparer());
            DateTime StartingDate = BalanceDate;
            Decimal StartingBalance = Balance;

            if (Transactions.Count > 0)
            {
                // Use first transaction date as opening balance date
                StartingDate = Transactions[0].DatePosted;

                // To calculate Opening Balance i need to negatively apply all transactions
                foreach (QFXTransaction trans in Transactions)
                {
                    StartingBalance -= trans.TransactionAmount;
                }

                StartDate = StartingDate;

                // HACK : If using this class in another application you might want to comment out the line
                // below which encodes a 'starting balance' into the transaction list
                QFXTransaction starttrans = new QFXTransaction("", StartingDate, StartingBalance, "ChromaticReconcile"+StartingDate.ToShortDateString(), "ChromaticReconcile", "");
                Transactions.Add(starttrans);
            }

            EndDate = BalanceDate;

            //QFXTransaction endtrans = new QFXTransaction("", BalanceDate, Balance, "ChromaticReconcile", "", "");
            //Transactions.Add(endtrans);

            Transactions.Sort(new TransactionComparer());
        }

        public void ParseString(string QFXString)
        {
            // ROUGH PROCEDURE :
            // Convert to lines
            string[] Lines = QFXString.Split('\n');
            List<string> OrgLines = new List<string>();
            List<string> CurrTransaction = new List<string>();
            List<string> LedgerBalLines = new List<string>();

            ParsingStateEnum ParsingState = ParsingStateEnum.INIT;

            // Skip cursor to ORG tag
            foreach (string line in Lines)
            {
                if (line.Contains("<ORG>")) ParsingState = ParsingStateEnum.ORG;

                if (line.Contains("<STMTTRN>")) 
                {
                    // Since there is no </ORG> tag, use first trans as flag 
                    if (ParsingState == ParsingStateEnum.ORG)  ParseOrg(OrgLines);
                    ParsingState = ParsingStateEnum.TRANS;
                }

                if (line.Contains("</STMTTRN>")) {
                    ParseTransaction(CurrTransaction);
                    CurrTransaction.Clear();
                }

                if (line.Contains("<LEDGERBAL>")) ParsingState = ParsingStateEnum.LEDGERBAL;

                if (line.Contains("</LEDGERBAL>") || line.Contains("</ORG>")) 
                {
                    ParseLedgerBal(LedgerBalLines);
                    break;
                }

                switch (ParsingState)
                {
                    case ParsingStateEnum.INIT: break;
                    case ParsingStateEnum.ORG: OrgLines.Add(line); break;
                    case ParsingStateEnum.TRANS: CurrTransaction.Add(line); break;
                    case ParsingStateEnum.LEDGERBAL: LedgerBalLines.Add(line); break;
                    default : break;
                }
            }

        }

        void ParseOrg(List<string> OrgLines)
        {
            foreach (string line in OrgLines)
            {
                if (line.Contains("<ORG>")) Organization = line.Replace("<ORG>", "").Trim();
                if (line.Contains("<CURDEF>")) Currency = line.Replace("<CURDEF>", "").Trim();
                if (line.Contains("<BANKID>")) BankID = line.Replace("<BANKID>", "").Trim();
                if (line.Contains("<ACCTID>")) AccountID = line.Replace("<ACCTID>", "").Trim();
                if (line.Contains("<ACCTTYPE>")) AccountType = line.Replace("<ACCTTYPE>", "").Trim();
                if (line.Contains("<INTU.BID>")) IntuitBankID = line.Replace("<INTU.BID>", "").Trim();
            }
        }

        void ParseTransaction(List<string> TransactionLines) 
        {
            string TransactionType = "";         
            DateTime DatePosted = DateTime.MinValue;            
            Decimal TransactionAmount = Decimal.MinValue;
            string FitID = "";
            string Name = "";                    
            string Memo = "";                    

            foreach (string line in TransactionLines)
            {
                if (line.Contains("<TRNTYPE>")) TransactionType = line.Replace("<TRNTYPE>", "").Trim();
                if (line.Contains("<DTPOSTED>"))
                {
                    // 20090430160000 = 04/30/2009
                    string DateString = line.Replace("<DTPOSTED>", "").Trim();
                    int YearVal = int.Parse(DateString.Substring(0, 4));
                    int MonthVal = int.Parse(DateString.Substring(4, 2));
                    int DayVal = int.Parse(DateString.Substring(6, 2));

                    DatePosted = new DateTime(YearVal, MonthVal, DayVal);
                }
                if (line.Contains("<TRNAMT>")) TransactionAmount = Decimal.Parse(line.Replace("<TRNAMT>", "").Trim());
                if (line.Contains("<FITID>")) FitID = line.Replace("<FITID>", "").Trim();
                if (line.Contains("<NAME>")) Name = line.Replace("<NAME>", "").Trim();
                if (line.Contains("<MEMO>")) Memo = line.Replace("<MEMO>", "").Trim();
            }

            QFXTransaction trans = new QFXTransaction(TransactionType, DatePosted, 
                TransactionAmount, FitID, Name, Memo);

            Transactions.Add(trans);
        }

        void ParseLedgerBal(List<string> LedgerBalLines)
        {
            foreach (string line in LedgerBalLines)
            {
                if (line.Contains("<BALAMT>")) Balance = Decimal.Parse(line.Replace("<BALAMT>", "").Trim());
                if (line.Contains("<DTASOF>"))
                {
                    string DateString = line.Replace("<DTASOF>", "").Trim();
                    int YearVal = int.Parse(DateString.Substring(0, 4));
                    int MonthVal = int.Parse(DateString.Substring(4, 2));
                    int DayVal = int.Parse(DateString.Substring(6, 2));

                    BalanceDate = new DateTime(YearVal, MonthVal, DayVal);
                }
            }
        }

        #endregion

        #region Filtering

        public Dictionary<string, Decimal> GroupTransactionsByName(DateTime StartDate, DateTime EndDate)
        {
            Dictionary<string, Decimal> groupedTotals = new Dictionary<string, Decimal>();

            foreach (QFXTransaction t in Transactions)
            {
                if (t.DatePosted >= StartDate && t.DatePosted <= EndDate)
                {
                    Decimal groupTotal = 0.0m;

                    try
                    {
                        Decimal amt = groupedTotals[t.Name];
                        amt += t.TransactionAmount;
                        groupedTotals[t.Name] = amt;
                    }
                    catch (System.Collections.Generic.KeyNotFoundException ex)
                    {
                        Decimal newTotal = t.TransactionAmount;

                        groupedTotals.Add(t.Name, newTotal);
                    }
                }
            }

            return groupedTotals;
        }

        public List<QFXTransaction> FilterByCategory(string CategoryName)
        {
            List<QFXTransaction> filtered = new List<QFXTransaction>();

            foreach (QFXTransaction trans in Transactions)
            {
                if (trans.Category == CategoryName && trans.Name != "ChromaticReconcile") 
                    filtered.Add(trans);
            }

            return filtered;
        }

        public Decimal TotalCategory(string CategoryName, DateTime StartDate, DateTime EndDate)
        {
            Decimal amt = 0.0m;

            foreach (QFXTransaction trans in Transactions)
            {
                if (trans.Category == CategoryName && trans.Name != "ChromaticReconcile")
                {
                    if (trans.DatePosted >= StartDate && trans.DatePosted <= EndDate)
                        amt += trans.TransactionAmount;
                }
            }

            return amt;
        }

        public List<QFXNormalization> NormalizeCategoryTotals(DateTime StartDate, DateTime EndDate)
        {
            TimeSpan ts = EndDate - StartDate;
            int NumberOfDays = ts.Days;

            Decimal DailyAverage = 0.0m, MonthlyAverage = 0.0m, YearlyAverage = 0.0m;

            List<QFXNormalization> normalized = new List<QFXNormalization>();

            foreach (string cat in Categories)
            {
                Decimal CategoryTotal = TotalCategory(cat, StartDate, EndDate);

                if (NumberOfDays == 0) DailyAverage = 0;
                else DailyAverage = CategoryTotal / NumberOfDays;

                MonthlyAverage = Decimal.Round(DailyAverage * 30.5m, 2);
                YearlyAverage = Decimal.Round(DailyAverage * 365, 2);
                DailyAverage = Decimal.Round(DailyAverage, 2);

                normalized.Add(new QFXNormalization(cat, DailyAverage, MonthlyAverage, YearlyAverage));
            }

            return normalized;
        }

        public List<QFXNormalization> NormalizeRawTransactions(DateTime StartDate, DateTime EndDate)
        {
            TimeSpan ts = EndDate - StartDate;
            int NumberOfDays = ts.Days;

            Decimal DailyAverage = 0.0m, MonthlyAverage = 0.0m, YearlyAverage = 0.0m;

            List<QFXNormalization> normalized = new List<QFXNormalization>();

            Dictionary<string, Decimal> groupedTrans = GroupTransactionsByName(StartDate, EndDate);

            foreach (KeyValuePair<string, Decimal> kvp in groupedTrans)
            {
                if (NumberOfDays == 0) DailyAverage = 0;
                else DailyAverage = kvp.Value / NumberOfDays;

                MonthlyAverage = Decimal.Round(DailyAverage * 30.5m, 2);
                YearlyAverage = Decimal.Round(DailyAverage * 365, 2);
                DailyAverage = Decimal.Round(DailyAverage, 2);

                normalized.Add(new QFXNormalization(kvp.Key, DailyAverage, MonthlyAverage, YearlyAverage));
            }

            return normalized;
        }



        #endregion
    }
}
